Android OpenGL的简单使用(10):GLES20的简单使用和点的绘制

Posted by alonealice on 2021-01-09

先上示例

1
2
3
4
5
6
7
if (isSupported()) {
//使用EGLS20
surfaceView.setEGLContextClientVersion(2);
surfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
surfaceView.setRenderer(getRenderer());
surfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@Override
protected GLSurfaceView.Renderer getRenderer() {
return new PointRenderer();
}

class PointRenderer implements GLSurfaceView.Renderer {

public static final String VERTEX_SHADER =
"attribute vec4 vPosition;\n"
+ "void main() {\n"
+ " gl_Position = vPosition;\n"
+ "}";
public static final String FRAGMENT_SHADER =
"precision mediump float;\n"
+ "void main() {\n"
+ " gl_FragColor = vec4(1, 1, 1, 1);\n" //设置颜色
+ "}";

//顶点数组
private final float[] VERTEX = { // in counterclockwise order:
0, 0, 0
};

// 缓冲区
private final FloatBuffer mBuffer;

public PointRenderer() {
mBuffer = Util.getFloatBuffer(VERTEX);
}

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20.glClearColor(0f, 0, 0f, 0f);

int mProgram = GLES20.glCreateProgram();
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);

GLES20.glAttachShader(mProgram, vertexShader);
GLES20.glAttachShader(mProgram, fragmentShader);
GLES20.glLinkProgram(mProgram);
GLES20.glUseProgram(mProgram);

int mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
GLES20.glEnableVertexAttribArray(mPositionHandle);

GLES20.glVertexAttribPointer(mPositionHandle, 1, GLES20.GL_FLOAT, false,
4, mBuffer);

}

private int loadShader(int type, String shaderCode) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}

@Override
public void onDrawFrame(GL10 gl) {
// 清除屏幕
GLES20.glClear(GL10.GL_COLOR_BUFFER_BIT);
//绘制顶点
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, 1);
}
}

代码解释

初始化

1
setEGLContextClientVersion(2);  //使用OpenGL ES 2.0

选择EGL配置

Android设备往往支持多种EGL配置,可以使用不同数目的通道(channel),也可以指定每个通道具有不同数目的位(bits)深度。因此, 在渲染器工作之前就应该指定EGL的配置。GLSurfaceView默认EGL配置的像素格式为RGB_656,16位的深度缓存(depth buffer),默认不开启遮罩缓存(stencil buffer)。

1
surfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);

设置渲染模式

RenderMode 有两种,RENDERMODE_WHEN_DIRTYRENDERMODE_CONTINUOUSLY,前者是懒惰渲染,需要手动调用 glSurfaceView.requestRender() 才会进行更新,而后者则是不停渲染。

1
surfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

GLSL 程序

和普通的 view 利用 canvas 来绘制不一样,OpenGL 需要加载 GLSL 程序,让 GPU 进行绘制。所以我们需要定义 shader 代码,并在初始化时(也就是 onSurfaceCreated 回调中)加载。

  • 创建 GLSL 程序:glCreateProgram

在连接shader之前,首先要创建一个容纳程序的容器,称为着色器程序容器。

  • 加载 shader 代码

glCreateShader:创建一个着色器对象,指定要创建的着色器的类型。 只能是 GL_VERTEX_SHADERGL_FRAGMENT_SHADER。**GL_VERTEX_SHADER **类型的着色器是一个用于在可编程顶点处理器上运行的着色器。 GL_FRAGMENT_SHADER 类型的着色器是一个着色器

glShaderSource:替换着色器对象中的源代码,glShaderSource 将着色器中的源代码设置为string指定的字符串数组中的源代码。先前存储在着色器对象中的任何源代码都将被完全替换。

glCompileShader:编译一个着色器对象,**glCompileShader 编译已存储在shader指定的着色器对象中的源代码字符串。

  • attatchshader 代码:glAttachShader

将着色器对象附加到program对象。

  • 链接 GLSL 程序:glLinkProgram

连接一个program对象。**glLinkProgram 链接program指定的program对象。附加到program的类型为GL_VERTEX_SHADER的着色器对象用于创建将在可编程顶点处理器上运行的可执行文件。 附加到program的类型为GL_FRAGMENT_SHADER的着色器对象用于创建将在可编程片段处理器上运行的可执行文件。

  • 使用 GLSL 程序:glUseProgram

使用程序对象作为当前渲染状态的一部分。通过使用 glAttachShader 成功将着色器对象附加到程序对象,使用 glCompileShader 成功编译着色器对象,并成功链接程序对象与 glLinkProgram,之后就可以在程序对象中创建每个阶段的可执行文件。

  • 获取 shader 代码中的变量索引:glGetAttribLocation

返回属性变量的位置。glGetAttribLocation 查询由program指定的先前链接的程序对象,用于name指定的属性变量,并返回绑定到该属性变量的通用顶点属性的索引。 如果name是矩阵属性变量,则返回矩阵的第一列的索引。 如果指定的属性变量不是指定程序对象中的活动属性,或者名称以保留前缀“gl_”开头,则返回-1。

  • 启用 vertex:glEnableVertexAttribArray

启用或禁用通用顶点属性数组。将访问通用顶点属性数组中的值,并在调用顶点数组命令( 如 glDrawArrays 或 glDrawElements )时用于呈现。

  • 绑定 vertex 坐标值:glVertexAttribPointer

glVertexAttribPointer 指定索引index处的通用顶点属性数组的位置和数据格式,以便在渲染时使用。

ndex:指定要修改的通用顶点属性的索引。

size:指定每个通用顶点属性的组件数。 必须为1,2,3或4.初始值为4。

type:指定数组中每个组件的数据类型。 接受符号常量GL_BYTEGL_UNSIGNED_BYTEGL_SHORTGL_UNSIGNED_SHORTGL_FIXED GL_FLOAT。 初始值为GL_FLOAT

normalized:指定在访问定点数据值时是应将其标准化(GL_TRUE)还是直接转换为定点值(GL_FALSE)。

stride:指定连续通用顶点属性之间的字节偏移量。 如果stride为0,则通用顶点属性被理解为紧密打包在数组中的。 初始值为0。

pointer:指定指向数组中第一个通用顶点属性的第一个组件的指针。 初始值为0。

绘制

1
2
//绘制顶点
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, 1);

投影变换

OpenGL 坐标系和安卓手机坐标系不是线性对应的,因为手机的宽高比几乎都不是 1。因此我们绘制的形状是变形的。要解决这个问题就可以使用投影变换。

使用较多的是正投影和透视投影,这里我们使用透视投影:Matrix.perspectiveM。通常坐标系的变换都是对顶点坐标进行矩阵左乘运算,因此我们需要修改我们的 vertex shader :

1
2
3
4
5
6
private static final String VERTEX_SHADER = 
"attribute vec4 vPosition;\n"
+ "uniform mat4 uMVPMatrix;\n"
+ "void main() {\n"
+ " gl_Position = uMVPMatrix * vPosition;\n"
+ "}";

onSurfaceCreated 中获取 uMVPMatrix 的索引:

1
2
3
4
5
6
7
8
9
@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// ...

mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
mMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");

// ...
}

onSurfaceChanged 中计算变换矩阵:

1
2
3
4
5
6
7
8
9
 private float[] mMVPMatrix = new float[16];

@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);

Matrix.perspectiveM(mMVPMatrix, 0, 45, (float) width / height, 0.1f, 100f);
Matrix.translateM(mMVPMatrix, 0, 0f, 0f, -2.5f);
}

最后我们在绘制时为 uMVPMatrix 赋值:

1
2
3
4
5
6
7
8
@Override
public void onDrawFrame(GL10 unused) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

GLES20.glUniformMatrix4fv(mMatrixHandle, 1, false, mMVPMatrix, 0);

GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}

使用的顶点数据:

1
2
3
4
5
6
//顶点数组
private final float[] VERTEX = { // in counterclockwise order:
0, 0, 0,
1, 0, 0,
1, 1, 0
};

未变换前,(1,0,0)在屏幕中间最右边,(1,1,0)在右上角。变换后,(1,1,0)在右边,与

(1,0,0)的距离跟(1,0,0)到(0,0,0)一样,即坐标系跟手机实际尺寸比例相等。

1
2
3
4
public static void perspectiveM(float[] m, int offset,
float fovy, float aspect, float zNear, float zFar)

Matrix.perspectiveM(mMVPMatrix, 0, 45, (float) width / height, 0.1f, 100f);

m 是保存变换矩阵的数组,offset 是开始保存的下标偏移量。fovy 是 y 轴的 field of view 值,也就是视角大小,视角越大,我们看到的范围就越广,例如 90° 和 45°, 90° 整个图像就会比45°小一点。aspect 是 Screen space 的宽高比。zNearzFar 则是视锥体近平面和远平面的 z 轴坐标了。

由于历史原因,Matrix.perspectiveM 会让 z 轴方向倒置,所以左乘投影矩阵之后,顶点 z 坐标需要在 -zNear~-zFar 范围内才会可见。

前面我们顶点的 z 坐标都是 0,我们可以把它修改为 -0.1f~-100f 之间的值,也可以通过一个位移变换来达到此目的:

1
Matrix.translateM(mMVPMatrix, 0, 0f, 0f, -2.5f);

我们沿着 z 轴的反方向移动 2.5,这样就能把 z 坐标移到 -0.1f~-100f 了。